iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 4

[Day 4] useThrottleFn - 加入 rejectOnCancel 之前,先來處理 this

  • 分享至 

  • xImage
  •  

在加入 rejectOnCancel 之前,先處理一下 this 的問題,今天主要會調整 Day2 提到的 Wrapper Function & Demo code,可以回到 Day2 搭配著看。也會對核心邏輯 throttleFilter 做一點小調整,可以跟 Day3 搭配著看~

調整 Demo code,重現 this 問題

<script setup>
// ... 略
function updateValue() {
  console.log('this: ', this) // undefined
  updated.value += 1
}

const throttledFn = useThrottleFn(updateValue, 2000)

function clickHandler() {
  clicked.value += 1
  const testThis = {}
  throttledFn.apply(testThis)
}
</script>
<!-- ... 略 -->

用 Function Declaration 的方式定義 updateValue,取代原本的 Arrow function 來觀察 this。
在 clickHandler 中使用 throttledFn.apply(testThis) 來綁定 this,預期在 updateValue 中的 this 應該要是 testThis,但目前因為沒做任何處理,所以是 undefined。

接下來的目標就是在 updateValue function 中拿到的 this 必須是 testThis 物件。

至於為什麼要做到這件事?在 Vue 中因為很少用 this,所以拿 Vanilla JS 舉例,在底下的例子應該就有很明確需要在 clickHandler 中拿到 this 的需求。

const clickEl = document.getElementById("clickEl");

function clickHandler () {
  console.log("this: ", this) // this 應該要是 clickEl
}

const clickHandlerThrottled = useThrottleFn(clickHandler, 2000, true, false);

clickEl.addEventListener("click", clickHandlerThrottled);

調整核心邏輯 throttleFilter

大致上跟昨天一樣,差別在原本會傳入 fn,現在改由 throttleFilter return 出去的那個 function 來接收這個 fn

export function throttleFilter(ms, trailing = false, leading = false) {
  // ...略
  
  // invoke 就是之前傳給 throttleFilter 的 fn 參數,現在不從 throttleFilter 接收 fn
  // 改由這個 return function 接收,所以原本 fn() 的地方會變成 invoke()
  return function (invoke) {
  
  }
}

調整 useThrottleFn

這邊主要新增了 createFilterWrapper,然後就像剛剛提到的 fn 不會再傳給 throttleFilter。

import { createFilterWrapper, throttleFilter } from '@/utils/filter'

export function useThrottleFn(fn, ms, trailing = false, leading = true) {
  return createFilterWrapper(
    throttleFilter(ms, trailing, leading),
    fn,
  )
}

新增 Wrapper Function - createFilterWrapper

綁定 this 的核心邏輯就是在這個 createFilterWrapper 中

// src/utils/filter.js
export function createFilterWrapper(filter, fn) {
  function wrapper(...args) {
    filter(() => fn.apply(this, args), { fn, this: this, args })
  };

  return wrapper
}

這時候我們一開始的 Demo code 中,updateValue 這個 function 就可以成功拿到 testThis 這個物件

蛤!剛剛這整段都在幹嘛?

接著可以從 Demo code 最上層一步一步往下走可能會比較清楚。
經過一連串的調整,再回頭過去看這行 const throttledFn = useThrottleFn(updateValue, 2000),會發現 throttledFn 是 createFilterWrapper 的回傳值,而 createFilterWrapper 回傳了 wrapper,也就是說 throttledFn 現在就是 wrapper。

所以當每次點擊觸發 clickHandler 時,就會執行 throttledFn.apply(testThis) 也就是 wrapper.apply(testThis),wrapper 被呼叫時會執行 filter(() => fn.apply(this, args), { fn, this: this, args }),這邊的 filter() 就是前面提到 throttleFilter 中 return 的那個 function,這個 function 接收一個 invoke 參數,可以看到這個 invoke 參數現在是 () => fn.apply(this, args),這個 fn 是我們傳入的 updateValue function,也就是說每次點擊的時候,都會執行 updateValue.apply(this, args)。所以這樣就解答了為什麼剛剛的調整可以讓 updateValue 這個 function 拿到正確的 this。

那為什麼會需要特別新增一個 createFilterWrapper 來做這件事呢?
因為有其他 vueuse api 也會共用到這個 wrapper function

export function useDebounceFn(
  fn,
  ms,
  options = {},
) {
  return createFilterWrapper(
    debounceFilter(ms, options),
    fn,
  )
}

把 createFilterWrapper 拿到 vueuse 原始碼中做全域搜尋,可以找到熟悉的 debounceFilter,用法看起來是一模一樣。


最新進度

為了方便觀看,先把最新版調整好的 code 完整放在這邊,明天會從今天的進度繼續~

useThrottleFn

// src/compositions/useThrottleFn.js 
import { createFilterWrapper, throttleFilter } from '@/utils/filter'

export function useThrottleFn(fn, ms, trailing = false, leading = true) {
  return createFilterWrapper(
    throttleFilter(ms, trailing, leading),
    fn,
  )
}

Filter Function

// src/utils/filter.js

export function createFilterWrapper(filter, fn) {
  function wrapper(...args) {
    filter(() => fn.apply(this, args), { fn, this: this, args })
  };

  return wrapper
}

export function throttleFilter(ms, trailing = false, leading = false) {
  let lastExec = 0
  let timer = null
  let isLeading = true

  return function (invoke) {
    const duration = Date.now() - lastExec

    if (timer) {
      clearTimeout(timer)
      timer = null
    }

    if (duration >= ms && (leading || !isLeading)) {
      lastExec = Date.now()
      invoke()
    }
    else if (trailing) {
      timer = setTimeout(() => {
        lastExec = Date.now()
        invoke()
        clearTimeout(timer)
        timer = null
      }, ms - duration)
    }

    if (!leading && !timer) {
      timer = setTimeout(() => {
        isLeading = true
      }, ms)
    }

    isLeading = false
  }
}

Demo

<!-- src/components/UseThrottleFnDemo.vue -->
<script setup>
import { ref } from 'vue'
import { useThrottleFn } from '@/compositions/useThrottleFn'

const clicked = ref(0)
const updated = ref(0)

const throttledFn = useThrottleFn(() => {
  updated.value += 1
})

function clickHandler() {
  clicked.value += 1
  throttledFn()
}
</script>

<template>
  <h2>UseThrottleFnDemo</h2>
  <div class="box" @click="clickHandler">
    <span>click throttle box</span>
  </div>
  <div>Button clicked: {{ clicked }}</div>
  <div>Event handler called: {{ updated }}</div>
</template>

GitHub:https://github.com/RhinoLee/30days_vue/pull/6/files


上一篇
[Day 3] useThrottleFn - 核心邏輯
下一篇
[Day 5] useThrottleFn - Promise & rejectOnCancel
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言